package hibatis.support;

import hibatis.annotation.Filter;
import hibatis.annotation.GeneratedSql;
import hibatis.annotation.HasMany;
import hibatis.annotation.HasOne;
import hibatis.support.parse.EntityMeta;
import hibatis.support.parse.MappedMethod;
import hibatis.support.parse.MapperStatementParser;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.exceptions.TooManyResultsException;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Created by huangdachao on 2018/6/22 22:27.
 */
@Intercepts(value = {
    @Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
            CacheKey.class, BoundSql.class}),
    @Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ResultInterceptor implements Interceptor {
    private static final Pattern FIELD_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(.[a-zA-Z_][a-zA-Z0-9_]*)*$");
    private static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("[#$]\\{([a-zA-Z0-9_.]+)}");

    @Override
    @SuppressWarnings("unchecked")
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        List result = (List) invocation.proceed();
        Executor exe = (Executor) invocation.getTarget();
        Configuration conf = ((MappedStatement) args[0]).getConfiguration();
        String mappedStatementId = ((MappedStatement) args[0]).getId();
        MappedMethod mm = MapperStatementParser.getMappedMethod(mappedStatementId);
        if (mm != null && mm.type == GeneratedSql.Type.SELECT && result.size() > 0) {
            List<Field> fields = new ArrayList<>();
            EntityMeta em = EntityMeta.get(mm.entityType);
            Map<String, List<String>> secondaryQueryMap = em.getSecondaryQueryFieldViewMap();
            for (Map.Entry<String, List<String>> entry : secondaryQueryMap.entrySet()) {
                for (String v : mm.views) {
                    if (entry.getValue().contains(v)) {
                        Field f = mm.entityType.getDeclaredField(entry.getKey());
                        f.setAccessible(true);
                        fields.add(f);
                        break;
                    }
                }
            }

            fields.forEach(f -> {
                for (Object o : result) {
                    if (!shouldQuery(o, f)) {
                        continue;
                    }
                    MappedStatement ms = conf.getMappedStatement(Reflection.formatField(f));
                    try {
                        List list = exe.query(ms, o, new RowBounds(), null);
                        Class<?> fieldType = f.getType();
                        if (fieldType.isAssignableFrom(ArrayList.class)) {
                            Collection col = new ArrayList(list.size());
                            col.addAll(list);
                            f.set(o, col);
                        } else if (fieldType.isAssignableFrom(HashSet.class)) {
                            Collection col = new HashSet(list.size());
                            col.addAll(list);
                            f.set(o, col);
                        } else if (Collection.class.isAssignableFrom(fieldType)) {
                            Collection col = (Collection) fieldType.newInstance();
                            col.addAll(list);
                            f.set(o, col);
                        } else if (fieldType.isArray()) {
                            Object[] arr = (Object[]) Array.newInstance(fieldType.getComponentType(), list.size());
                            for (int i = 0; i < arr.length; i++) {
                                arr[i] = list.get(i);
                            }
                            f.set(o, arr);
                        } else {
                            if (list.size() > 1) {
                                throw new TooManyResultsException("只需要1条，但查询结果" + list.size() + "条：" + ms.getId());
                            } else if (list.size() == 1) {
                                f.set(o, list.get(0));
                            }
                        }
                    } catch (SQLException | IllegalAccessException | InstantiationException e) {
                        throw new RuntimeException(e);
                    }
                }

            });
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private boolean shouldQuery(Object row, Field field) {
        HasOne ho = field.getAnnotation(HasOne.class);
        HasMany hm = field.getAnnotation(HasMany.class);
        List<Filter> filters;

        if (ho != null) {
            filters = Arrays.stream(ho.filter()).filter(f -> !f.ignore()).collect(Collectors.toList());
        } else if (hm != null) {
            filters = Arrays.stream(hm.filter()).filter(f -> !f.ignore()).collect(Collectors.toList());
        } else {
            return false;
        }

        if (filters.size() == 0) {
            return false;
        }
        for (Filter f : filters) {
            try {
                String placeholder = f.placeholder();
                Matcher matcher = SUBSTITUTION_PATTERN.matcher(placeholder);
                if (matcher.find()) {
                    placeholder = matcher.group(1);
                } else if (!FIELD_PATTERN.matcher(placeholder).find()) {
                    return true;
                }
                Field valField = row.getClass().getDeclaredField(placeholder);
                valField.setAccessible(true);
                Object v = valField.get(row);
                if (v == null) {
                    return false;
                }
                if (f.ignoreOnZero() && Reflection.isNumberZero(v)) {
                    return false;
                }
            } catch (IllegalAccessException | NoSuchFieldException e) {
                throw new RuntimeException(e);
            }
        }

        return true;
    }
}
